import functools
import struct
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QCloseEvent, QShowEvent
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import sys

from PyQt5.QtWidgets import QWidget

# 帮我们直接运行此文件时，可以加载到上级目录的ui包
sys.path.append("../")
from ui.Ui_main_window import Ui_MainWindow
from ui.Ui_pid_dialog import Ui_Dialog_pid

from drivers.driver_serial import *
from common.qt_worker import Worker
from common.utils import *
from common.protocol import *
from drivers.protocol_parser import *
"""
数据格式

功能码 + 数据
================发送===============
target: F0 00 00
    kp: E0 00 00 00 00 00
    ki: E0 01 00 00 00 00
    kd: E0 02 00 00 00 00

================接收===============
current        : B0 00 00
kp + ki + kd   : B1 00 00 00 00 FF FF FF FF 00 00 00 00

"""

SEND_KEY_MAP = {"pid": 0xE0, "target": 0xF0}
RECV_KEY_CURRENT = 0xB0
RECV_KEY_PID = 0xB1
RECV_KEY_ERROR = 0xB2
        
slider_style = """
QSlider::groove:horizontal{border: 1px solid #999999;height: 10px;background: #FFFFFF;margin: 0px 0px;}
QSlider::handle:horizontal{background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #F5F5F5, stop:1 #DCDCDC);border: 1px solid #999999;width: 20px;margin: -5px 0px;border-radius: 5px;}
"""
class PidViewDialog(QDialog):
    shown = pyqtSignal()
    closed = pyqtSignal()
    
    def __init__(self, parent = None) -> None:
        super().__init__(parent)
        self.ui = Ui_Dialog_pid()
        self.ui.setupUi(self)
        
        self.ui.btn_clear.clicked.connect(self.on_btn_clear_clicked)
    
    def on_btn_clear_clicked(self):
        self.ui.rp_pid.clear_data()
        
    def showEvent(self, a0: QShowEvent) -> None:
        self.shown.emit()
        return super().showEvent(a0)
        
    def closeEvent(self, a0: QCloseEvent) -> None:
        self.closed.emit()
        return super().closeEvent(a0)
        

class MyMainWindow(QMainWindow):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        
        self.cur_serial_device: SerialDevice = None
        self.refresh_worker: Worker = None
        self.recv_worker: Worker = None
        
        self.k_p = 1.0
        self.k_i = 1.0
        self.k_d = 1.0
        
        # 启用企业级协议
        self.is_enterprise_mode = True
        self.__load_mode()
        
        self.is_current_sync = False
        self.is_pid_sync = False
        
        self.__buffer = b''

        self.protocol_parser = ProtocolParser()
        self.protocol_parser.start(msg_handler=self.on_parsed_data_received)

        self.init_ui()
        self.dialog = PidViewDialog(self)
        self.dialog.closed.connect(self.on_dialog_pid_closed)
        
        self.refresh_serial_devices()
        
    def __load_mode(self):
        f = '.config'
        if os.path.exists(f):
            with open(f, 'r') as file:
                if "1" == file.read():
                    self.is_enterprise_mode = True
                else:
                    self.is_enterprise_mode = False
        else:
            self.is_enterprise_mode = False 
    
    def __set_mode(self, mode):
        f = '.config'
        content = "1" if mode else "0"
        with open(f, 'w') as file:
            file.write(content)

    def on_slider_target_valueChanged(self, value):
        # 构建字节数组，首字节为0xF0, value用两个字节表示
        # data = bytes([0xF0, value >> 8, value & 0xFF])
        self.dialog.ui.rp_pid.update_target(value)
        if self.ui.actionEnterprise.isChecked():
            data = pack_protocol_data_with_checksum(SEND_KEY_MAP["target"], value)
        else:
            data = pack_protocol_data(SEND_KEY_MAP["target"], value)
    
        msg = f"设置目标值：{value} -> {decode_to_hex_str(data)}"
        # 打印字节数组
        # print(msg)
        
        if self.cur_serial_device is None:
            self.statusBar().showMessage("请先打开串口.", 3000)
            return
        
        self.statusBar().showMessage(msg, 3000)
        self.cur_serial_device.write(data)
        # 添加到日志
        self.append_send_log(data)
        
    @pyqtSlot()
    def on_btn_clear_clicked(self):
        self.ui.edit_log.clear()
        
    def append_send_log(self, content):
        if not self.ui.cb_send.isChecked():
            return
        
        # 根据是否勾选16进制，选择不同的显示方式
        if self.ui.cb_hex.isChecked():
            self.ui.edit_log.appendPlainText(f"🔵发->{decode_to_hex_str(content)}")
        else :
            # 将字节数组转成对应的字符串
            self.ui.edit_log.appendPlainText(f"🔵发->{decode_data(content)}")

    def update_ui(self):
        """
        根据连接状态更新UI
        :return:
        """
        if self.cur_serial_device is not None:
            # 修改图标
            self.ui.label_connect_state.setPixmap(QPixmap(":/icon/conn"))
            # 修改按钮文字
            self.ui.btn_open_device.setText("关闭串口")
        else:
            # 修改图标
            self.ui.label_connect_state.setPixmap(QPixmap(":/icon/disc"))
            # 修改按钮文字
            self.ui.btn_open_device.setText("打开串口")

    def close_serial_device(self):
        if self.cur_serial_device is not None:
            self.cur_serial_device.close()
            self.cur_serial_device = None

        self.update_ui()

    def on_refresh_serial_port_result(self, ports):
        self.ui.cb_devices.clear()
        # 添加每个元素1作为文本，元素0作为数据
        for port in ports:
            device, description = port
            self.ui.cb_devices.addItem(description, port)

        self.statusBar().showMessage(
            f"刷新完毕，{len(ports)} serial ports found.", 5000
        )

    def refresh_serial_devices(self):
        self.refresh_worker = Worker(scan_serial_ports)
        self.refresh_worker.signal_connect(
            result_handler=self.on_refresh_serial_port_result
        )
        self.refresh_worker.start()

    @pyqtSlot()
    def on_btn_serial_refresh_clicked(self):
        if self.refresh_worker is not None and self.refresh_worker.is_running:
            return

        self.refresh_serial_devices()

    def receive_data(self, worker: Worker):
        send_flag = True
        while worker.is_running:
            send_flag = True
            if self.cur_serial_device is None:
                break
            if not self.cur_serial_device.is_open():
                break
            if self.is_enterprise_mode:
                data = self.cur_serial_device.read()
            else:
                arr = self.cur_serial_device.readline()
                if arr:
                    d_len = len(arr)
                    if d_len >= 2 and arr[d_len - 2] == 0x0d and arr[d_len - 1] == 0x0a:
                        data = self.__buffer + arr
                        self.__buffer = b''
                    else:
                        self.__buffer += arr
                        send_flag = False
                    
            # print("data: ", data)
            if data is not None:
                if send_flag: worker.emit_msg(data)
            else:
                print("读取超时，或已断开！")
                break
        print("recv----------end")
         
    def on_parsed_data_received(self, response: Response):
        # print("on_parsed_data_received: ", response)
        if isinstance(response, ChannelResponse):
            print("channel response: ", response.values)
            self.update_value_pid(response.values[0], response.values[1], response.values[2])
        elif isinstance(response, PositionResponse):
            print("position response: ", response.values)
            self.update_value_current(response.values[0])
            
        # 将list转成bytes
        data = bytes(response.origin_array)
        
        self.append_recv_log(data)

    def append_recv_log(self, data):
        if not self.ui.cb_recv.isChecked():
            return
        if self.ui.cb_hex.isChecked():
            hex_content = decode_to_hex_str(data)
            self.ui.edit_log.appendPlainText(f"🟩收->{hex_content}")
        else:
            normal_content = decode_data(data)
            self.ui.edit_log.appendPlainText(f"🟩收->{normal_content}")
         
    def update_value_current(self, value):
        # 设置到界面上
        self.ui.slider_current.setValue(value)
        # 更新到图表
        self.dialog.ui.rp_pid.update_plot(value)
        
        if self.is_current_sync == False:
            self.ui.slider_target.setValue(value)
        # 更新状态&更新时间
        self.is_current_sync = True
        self.ui.label_sync_state.setText("已同步✅")
        self.ui.label_sync_time.setText(QDateTime.currentDateTime().toString("HH:mm:ss"))
         
    def update_value_pid(self, kp, ki, kd):
        # 更新数值，但不触发valueChanged信号
        # self.ui.sb_kp.blockSignals(True)
        # self.ui.sb_ki.blockSignals(True)
        # self.ui.sb_kd.blockSignals(True)
        self.ui.sb_kp.setValue(kp)
        self.ui.sb_ki.setValue(ki)
        self.ui.sb_kd.setValue(kd)
        # self.ui.sb_kp.blockSignals(False)
        # self.ui.sb_ki.blockSignals(False)
        # self.ui.sb_kd.blockSignals(False)
        
        # 更新状态&更新时间
        self.ui.label_sync_state_2.setText("已同步✅")
        self.ui.label_sync_time_2.setText(QDateTime.currentDateTime().toString("HH:mm:ss"))
        self.is_pid_sync = True
         
    @pyqtSlot(bytes)
    def on_serial_data_received(self, data: bytes):
        if self.is_enterprise_mode and self.protocol_parser is not None:
            self.protocol_parser.handle(data)
            return
        # print("on_serial_data_signal: ", decode_data(data))
        # 先处理数据，然后显示到界面上
        # 如果是0x01开头的数据，表示是当前的值
        # 格式如下：数据格式： 0x01 current[2] angle[4] 0x0A
        # for d in data:
        #     print('{:02X}'.format(d), end=' ')
        # print()
        if data[0] == RECV_KEY_CURRENT and len(data) == (1 + 2 + 2):
            print("recv_current: ", decode_to_hex_str(data))
            # 获取当前值
            current = struct.unpack('>h', data[1:3])[0]
            self.update_value_current(current)
            
        elif data[0] == RECV_KEY_PID and len(data) == (1 + 12 + 2):
            print("recv_pid: ", decode_to_hex_str(data))
            # 读取Kp值
            kp = struct.unpack('f', data[1:5])[0]
            # 读取Ki值
            ki = struct.unpack('f', data[5:9])[0]
            # 读取Kd值
            kd = struct.unpack('f', data[9:13])[0]
            # 设置到界面上
            self.update_value_pid(kp, ki, kd)        
        # elif data[0] == RECV_KEY_ERROR and len(data) == (1 + 4 + 2):
        #     error = struct.unpack('f', data[1:5])[0]
        #     self.dialog.ui.rp_pid.update_plot(error)
        self.append_recv_log(data)
               
    def open_serial_device(self, serial_device, baudrate):
        port, description = serial_device
        serial_device = SerialDevice(port, baudrate)
        success, msg = serial_device.open()
        if not success:
            self.statusBar().showMessage(f"设备{description}连接失败：{msg}", 3000)
            self.cur_serial_device = None
            return

        self.cur_serial_device = serial_device
        tip_msg = f"设备【{description}】连接成功."
        self.statusBar().showMessage(tip_msg, 3000)
        print(tip_msg)
        self.update_ui()
        
        self.is_pid_sync = False
        self.ui.label_sync_state.setText("未同步❌")
        self.ui.label_sync_time.setText(QDateTime.currentDateTime().toString("HH:mm:ss"))
        self.ui.label_sync_state_2.setText("未同步❌")
        self.ui.label_sync_time_2.setText(QDateTime.currentDateTime().toString("HH:mm:ss"))

        # 开启WorkerThread
        self.recv_worker = Worker(self.receive_data).signal_connect(
            msg_handler=self.on_serial_data_received,
            finished_handler=self.close_serial_device
        )
        self.recv_worker.start()

    @pyqtSlot()
    def on_btn_open_device_clicked(self):
        if self.cur_serial_device is not None:
            self.close_serial_device()
            return

        # check devices count
        if self.ui.cb_devices.count() == 0:
            self.statusBar().showMessage("请先扫描设备.", 5000)
            return

        # get selected device
        serial_device = self.ui.cb_devices.currentData()
        baudrate = int(self.ui.cb_baudrate.currentText())
        self.open_serial_device(serial_device, baudrate)

    def on_cb_hex_stateChanged(self, state):
        if state == Qt.Checked:
            self.ui.edit_log.clear()
            self.ui.edit_log.setPlaceholderText("Hex显示")
        else:
            self.ui.edit_log.clear()
            self.ui.edit_log.setPlaceholderText("ASCII显示")

    def on_pid_update_click(self, cmd, value):
        # print(f"on_pid_update: {clazz}, {value}")
        # 根据clazz的值，设置对应的值
        if cmd == "kp":
            self.ui.sb_kp.setValue(self.k_p * (1 + value * 0.01))
        elif cmd == "ki":
            self.ui.sb_ki.setValue(self.k_i * (1 + value * 0.01))
        elif cmd == "kd":
            self.ui.sb_kd.setValue(self.k_d * (1 + value * 0.01))
        
    def on_pid_box_update(self, cmd, value):
        # print(f"on_pid_box_update: {cmd}, {value}")
        # 根据cmd，发送对应的值
        param_type = b'\x00'
        if cmd == "kp":
            param_type = b'\x00'
            self.k_p = value
        elif cmd == "ki":
            param_type = b'\x01'
            self.k_i = value
        elif cmd == "kd":
            param_type = b'\x02'
            self.k_d = value
        
        # 构建字节数组
        # value_new用4个字节表示
        # 将value_new转成4个字节(大端模式MSB, 先发高位再发低位)
        # data = bytes([SEND_KEY_MAP[cmd]]) + struct.pack('f', value_new)
        if self.is_enterprise_mode:
            data = pack_protocol_data_with_checksum(SEND_KEY_MAP["pid"], param_type, value)
        else:
            data = pack_protocol_data(SEND_KEY_MAP["pid"], param_type, value)
        # 打印字节数组
        print(f"数据: {decode_to_hex_str(data)}")
        
        # 如果未同步数据，就先不让发
        if not self.is_pid_sync:
            self.statusBar().showMessage("请先等待同步PID完成后, 再修改PID数据.", 3000)
            return
        
        if self.cur_serial_device is None:
            self.statusBar().showMessage("打开串口才能发送数据", 3000)
            return
        
        # 发送数据
        self.cur_serial_device.write(data)
        # 添加到日志
        self.append_send_log(data)

    def on_action_simple_triggered(self):
        self.ui.actionSimple.setChecked(True)
        # 取消选中其他action
        self.ui.actionEnterprise.setChecked(False)
        print("启用简易协议")
        self.is_enterprise_mode = False
        self.__set_mode(False)

    def on_action_enterprise_triggered(self):
        self.ui.actionEnterprise.setChecked(True)
        # 取消选中其他action
        self.ui.actionSimple.setChecked(False)
        print("启用企业级协议")
        self.is_enterprise_mode = True
        self.__set_mode(True)
    
    def on_dialog_pid_closed(self):
        self.ui.actionPID.setChecked(False)
    
    def on_action_pidview_triggered(self):
        pid_show = self.ui.actionPID.isChecked()
        self.dialog.ui.rp_pid.update_target(self.ui.slider_target.value())
        if pid_show:
            self.dialog.show()
        else:
            self.dialog.close()

    def init_ui(self):
        # 禁止用户滑动slider_current，但是保留其样式
        self.ui.slider_current.setDisabled(True)
        # 设置slider_current的样式
        self.ui.slider_current.setStyleSheet(slider_style)
        
        # 设置两个action的checked状态监听
        self.ui.actionSimple.triggered.connect(self.on_action_simple_triggered)
        self.ui.actionEnterprise.triggered.connect(self.on_action_enterprise_triggered)
        self.ui.actionPID.triggered.connect(self.on_action_pidview_triggered)
        
        self.ui.actionSimple.setChecked(not self.is_enterprise_mode)
        self.ui.actionEnterprise.setChecked(self.is_enterprise_mode)
        
        self.ui.sb_kp.valueChanged.connect(functools.partial(self.on_pid_box_update, "kp"))
        self.ui.sb_ki.valueChanged.connect(functools.partial(self.on_pid_box_update, "ki"))
        self.ui.sb_kd.valueChanged.connect(functools.partial(self.on_pid_box_update, "kd"))
        
        self.ui.btn_p_up_10.clicked.connect(functools.partial(self.on_pid_update_click, "kp", 10))
        self.ui.btn_p_down_10.clicked.connect(functools.partial(self.on_pid_update_click, "kp", -10))
        self.ui.btn_p_up_1.clicked.connect(functools.partial(self.on_pid_update_click, "kp", 1))
        self.ui.btn_p_down_1.clicked.connect(functools.partial(self.on_pid_update_click, "kp", -1))

        self.ui.btn_i_up_10.clicked.connect(functools.partial(self.on_pid_update_click, "ki", 10))
        self.ui.btn_i_down_10.clicked.connect(functools.partial(self.on_pid_update_click, "ki", -10))
        self.ui.btn_i_up_1.clicked.connect(functools.partial(self.on_pid_update_click, "ki", 1))
        self.ui.btn_i_down_1.clicked.connect(functools.partial(self.on_pid_update_click, "ki", -1))

        self.ui.btn_d_up_10.clicked.connect(functools.partial(self.on_pid_update_click, "kd", 10))
        self.ui.btn_d_down_10.clicked.connect(functools.partial(self.on_pid_update_click, "kd", -10))
        self.ui.btn_d_up_1.clicked.connect(functools.partial(self.on_pid_update_click, "kd", 1))
        self.ui.btn_d_down_1.clicked.connect(functools.partial(self.on_pid_update_click, "kd", -1))

if __name__ == "__main__":
    app = QApplication(sys.argv)

    window = MyMainWindow()
    window.show()

    sys.exit(app.exec_())
